﻿using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Reflection;
using System.Diagnostics.Contracts;

namespace ReactiveLang.State
{
    using Util;

    partial class ChangeNotifications
    {
        static ChangeNotifications()
        {
#if CONTRACT_DEBUGGING
            ContractDebugging.Initialize();
#endif

            RegisterNotifier<PropertyInfo>(IExpressionValueNotifier);
            RegisterNotifier<PropertyInfo>(NotifyPropertyChangedNotifier);
            RegisterNotifier<PropertyInfo>(DependencyPropertyNotifier);
            RegisterNotifier<MethodInfo>(AttachedDependencyPropertyNotifier);
            RegisterNotifier<MemberInfo>(NamedEventNotifier);
            RegisterNotifier<PropertyInfo>(TimeNotifier);
        }

        #region Named events

        static ChangeNotifier NamedEventNotifier(MemberInfo member)
        {
            Contract.Requires(member != null);
            var bindingFlags = BindingFlags.Public | (member.IsStatic() ? BindingFlags.Static : BindingFlags.Instance);
            var changedEvent = member.DeclaringType.GetEvent(GetPropertyName(member) + "Changed", bindingFlags);
            if (changedEvent == null)
                return null;

            return (args, _) => Expression.Event(args.First(), changedEvent);
        }

        #endregion

        #region INotifyPropertyChanged

        static EventInfo _propertyChangedEvent = typeof(INotifyPropertyChanged).GetEvent("PropertyChanged");
        static ChangeNotifier NotifyPropertyChangedNotifier(PropertyInfo property)
        {
            if (property.IsStatic() || !typeof(INotifyPropertyChanged).IsAssignableFrom(property.DeclaringType))
                return null;

            Contract.Assume(_propertyChangedEvent != null);
            return (args, _) => Expression.Event<PropertyChangedEventArgs>(args.First(), _propertyChangedEvent).AsUnit();
        }

        #endregion

        #region Dependency properties

        static ChangeNotifier ObserveDependencyProperty(Type containerType, string dependencyPropertyName)
        {
            var depPropField = containerType.GetField(dependencyPropertyName + "Property", BindingFlags.Public | BindingFlags.Static);
            if (depPropField == null || !typeof(DependencyProperty).IsAssignableFrom(depPropField.FieldType))
                return null;
            var depProp = (DependencyProperty)depPropField.GetValue(null);
            return (args, _) => Expression.CreateWithDisposable<Unit>(p => 
            {
                var parent = ExpressionParent.Create<object>(__ => p.OnValue(default(Unit)), p.OnException);
                return new DependencyPropertyExpressionActivation((DependencyObject)args.First(), depProp, parent);
            });
        }
        static ChangeNotifier DependencyPropertyNotifier(PropertyInfo property)
        {
            Contract.Requires(property != null);
            if (property.IsStatic() || !typeof(DependencyObject).IsAssignableFrom(property.DeclaringType))
                return null;

            return ObserveDependencyProperty(property.DeclaringType, property.Name);
        }
        static ChangeNotifier AttachedDependencyPropertyNotifier(MethodInfo method)
        {
            if (method == null || method.DeclaringType == null || !method.IsStatic || method.IsGenericMethod || !StartsWithGetOrSet(method.Name))
                return null;

            int paramCount = method.Name.StartsWith("get", StringComparison.InvariantCultureIgnoreCase) ? 1 : 2;

            var parameters = method.GetParameters();
            if (parameters.Length != paramCount || !typeof(DependencyObject).IsAssignableFrom(parameters[0].ParameterType))
                return null;

            return ObserveDependencyProperty(method.DeclaringType, method.Name.Substring(3));
        }

        #endregion

        #region Time

        static TimeSpan _defaultInterval = TimeSpan.FromSeconds(1);
        public static TimeSpan DefaultInterval
        {
            get { return _defaultInterval; }
            set
            {
                Contract.Requires<ArgumentException>(value > TimeSpan.Zero, "value");
                _defaultInterval = value;
            }
        }

        static ChangeNotifier TimeNotifier(PropertyInfo property)
        {
            bool isAboutTime = (property.DeclaringType == typeof(DateTime) && (property.Name == "Today" || property.Name == "Now")) ||
                               (property.DeclaringType == typeof(Environment) && property.Name == "TickCount");
            if (!isAboutTime)
                return null;

            TimeSpan initialInterval = property.Name == "TickCount"
                ? TimeSpan.FromTicks(1)
                : DefaultInterval;

            return (args, options) => Expression.CreateWithDisposable<Unit>(p =>
            {
                var sync = new Sync();

                bool scheduled = false;
                var timer = new System.Threading.Timer(
                    delegate
                    {
                        if (scheduled)
                            return;
                        scheduled = true;
                        sync.Post(delegate
                        {
                            scheduled = false;
                            p.OnValue(default(Unit));
                        });
                    },
                    null,
                    TimeSpan.Zero,
                    initialInterval);

                IDisposable dispose = timer;

                if (options != null && options.Interval != null)
                    dispose = dispose.Concat(
                        options.Interval.ActivateDerived(p, s => timer.Change(TimeSpan.Zero, s))
                    );

                return dispose;
            });
        }

        #endregion

        #region IExpression.Value

        static ChangeNotifier IExpressionValueNotifier(PropertyInfo property)
        {
            if (property.DeclaringType == null || property.Name != "Value" || !typeof(IExpression<>).MakeGenericType(property.PropertyType).IsAssignableFrom(property.DeclaringType))
                return null;

            var asUnit = typeof(Expression).GetMethod("AsUnit", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
            Contract.Assume(asUnit != null);
            return (args, _) => (IExpression<Unit>)
                asUnit.MakeGenericMethod(ReflectionUtil.GetElementType(args.First().GetType()))
                      .Invoke(null, new[] { args.First() });
        }

        #endregion

        #region Util

        static bool StartsWithGetOrSet(string text)
        {
            return text.StartsWith("get", StringComparison.InvariantCultureIgnoreCase) || text.StartsWith("set", StringComparison.InvariantCultureIgnoreCase);
        }
        static string RemovePrefix(string text)
        {
            if (StartsWithGetOrSet(text))
                text = text.Substring(3);
            return text;
        }
        static string GetPropertyName(MemberInfo member)
        {
            return RemovePrefix(member.Name);
        }

        #endregion
    }
}
